Evaluating a machine learning model on the data used to train it reveals very little about how well it will perform in practice. Just as a student can score 100% on an exam by memorizing the answer key, a model can achieve near-zero training error simply by memorizing its input data. This chapter focuses on the methodologies and tools that separate a model that has genuinely learned underlying patterns from one that has merely memorized noise.
We begin by distinguishing between internal model parameters and user-configured hyperparameters. Next, we build up formal evaluation protocols—moving from simple hold-out and three-way splits to bias–variance diagnostics, learning curves, and k-fold cross-validation. Finally, we connect these concepts to practical scikit-learn workflows, showing how to execute systematic hyperparameter tuning without contaminating your final evaluation.
Reliable model evaluation begins by separating what the model learns from what the practitioner chooses, then using disciplined validation procedures to understand generalization, diagnose fitting behavior, and select hyperparameters without contaminating the final test estimate.
A model has two different kinds of settings, and it is important to keep them apart. Some are found by the learning algorithm itself, while others must be fixed by the user before training begins. The table below contrasts the two.
Learned automatically from training data during fitting.
| Algorithm | Parameters |
|---|---|
| Linear / Logistic Regression | Coefficient vector β and intercept β₀ |
| Neural Network | Weights W and biases b of every connection |
| Decision Tree | Actual split conditions and thresholds at each node |
| KNN | (None — KNN stores all training points directly) |
Set by the user before training starts. They control the learning process itself.
| Algorithm | Hyperparameters |
|---|---|
| KNN | k (neighbors), weights (uniform/distance), metric, p (Minkowski) |
| Neural Network | Learning rate α, #layers, #neurons/layer, batch size |
| Decision Tree | max_depth, min_samples_leaf, splitting criterion (gini/entropy) |
| Ridge/Lasso Regression | Regularization strength α |
Having distinguished parameters from hyperparameters, the central question becomes: how do our hyperparameter choices dictate performance? Hyperparameters directly govern model complexity—the flexibility of a model to fit patterns in the data. This complexity sits in direct tension with generalization, which is the model's ability to perform well on unseen data. k-Nearest Neighbors (k-NN) illustrates this trade-off clearly, as adjusting a single hyperparameter (k) moves the model across the entire complexity spectrum.
| K value (KNN) | # Effective Parameters | Model Complexity | Typical Behavior |
|---|---|---|---|
| K = 1 | 0 (stores data) | Highest | Memorizes every training point |
| K = 3 | 0 | High | Very flexible, jagged boundaries |
| K = 10 | 0 | Medium | Moderately smooth |
| K = 100 | 0 | Low | Smooth, simple boundaries |
| K = n (all points) | 0 | Lowest | Constant majority-class baseline |
Comparing performance across training and held-out data reveals whether a model has learned true underlying patterns or merely memorized noise. When model complexity is set too low or too high, performance degrades in predictable ways. The tabs below detail the two classic failure regimes—underfitting and overfitting—alongside the balanced state we aim to achieve.
To understand the root causes of underfitting and overfitting, we turn to the bias–variance decomposition. This framework provides a formal mathematical foundation for generalization by breaking down a model's expected prediction error on unseen data into three distinct sources:
Consider evaluating model performance across multiple independent training sets drawn from the same data source:
Under this framework:
Mathematical interpretation. The key mathematical detail is that \( \bar{\hat{y}} \) is computed separately for each input point: we hold \(x\) fixed and average the predictions obtained from many different training datasets, while the bias and variance themselves are evaluated at that fixed point.
To make this precise, suppose the observed target at a particular input \(x\) is generated by
Let \( \hat{y}_D=\hat{f}_D(x) \) be the prediction produced by a model trained on one particular dataset \(D\), and let
be its average prediction at the same \(x\) across repeated training datasets. The expected squared prediction error then decomposes as:
When we consider many test points, we repeat this decomposition for each \(x_i\) and then sum or average the resulting errors across those points.
The decomposition compares predictions with the underlying, noise-free function \(f(x)\), not simply with one observed target \(y\). In real-world data, we usually observe only \(y=f(x)+\varepsilon\), so we usually cannot directly calculate the true Bias² and irreducible noise for a particular problem. The value of the decomposition in practice is therefore mainly diagnostic: it gives us a framework for understanding whether poor performance is mainly due to an overly simple model (high bias) or an overly sensitive model (high variance).
Suppose that for one test point \(x_0\), the true function value is \(f(x_0)=10\). We train the same model on three different training datasets, obtaining predictions:
Step 1 — Average prediction:
Step 2 — Bias²: The average prediction is 9, while the true function value is 10.
Step 3 — Variance: The individual predictions fluctuate around their average of 9.
Interpretation: The model has some systematic error because its average prediction is below the truth, giving Bias² = 1. It also has prediction variability because different training datasets produce different predictions, giving Variance ≈ 2.67. A model with predictions that stay close to one another but consistently miss the true value would have high bias and low variance; a model whose average is close to the truth but whose predictions swing widely would have low bias and high variance. The same logic scales up: training on 100 datasets instead of 3 just gives a more reliable estimate of \(\bar{\hat{y}}\) and the spread around it.
Irreducible noise is different: even if the model has zero bias and zero variance, the observed target can still differ from \(f(x)\) because of randomness or measurement noise in the data-generating process. This is the part no learning algorithm can eliminate.
Because irreducible noise sets a hard lower bound on prediction error, model optimization is fundamentally a balancing act: finding the complexity at which the sum of Bias² and Variance is minimized. In practice, though, we cannot compute Bias² and Variance directly, since \(f(x)\) is unknown — that's the caveat noted above. The sections that follow — two-way and three-way splits, cross-validation, and learning curves — are the practical tools used to estimate whether a model's error is dominated by bias or by variance, so that its complexity can be adjusted accordingly.
To measure generalization empirically rather than relying on theoretical assumptions, we must establish a disciplined data-splitting protocol. The simplest and most intuitive approach is the hold-out method, which isolates a portion of our data solely for final evaluation.
The model is fitted on the training set only, and the test set is held out until the end. The test set error estimates generalization error (out-of-sample error) — how well the model will perform on truly unseen data. Low training error + high test error = overfitting.
If we reuse the test set repeatedly for model selection or hyperparameter tuning, it effectively becomes part of the training data and the model overfits to the test set. The reported scores then become optimistic: they are inflated, misleading, and not reproducible on truly unseen data.
A standard two-way split works well for evaluating a single, fixed model. However, when we repeatedly tune hyperparameters against the test set, information leaks from the test set into our design decisions. To prevent this data contamination, we expand our architecture into a three-way split.
While a three-way split provides a clean separation of data, discarding 15–20% of a small dataset solely for validation can significantly degrade model quality. K-fold cross-validation resolves this trade-off by systematically rotating the validation set across the entire training cohort.
The hold-out validation estimates are sensitive to exactly which rows landed in the validation split. K-fold fixes this by repeating the process k times on different partitions and averaging:
Standard value: k = 10 (10-fold CV) is the default in nearly every ML paper. Stratified k-fold (for classification) ensures each fold has roughly the same class distribution as the whole dataset.
In standard K-fold cross-validation, data is split into a small number of equal folds (typically k = 5 or k = 10). However, a natural boundary case arises when we push k to its logical extreme: setting k = n, where n is the total number of instances in the dataset. This variation is known as Leave-One-Out Cross-Validation (LOOCV).
In LOOCV, each individual fold consists of a single observation. For every iteration, the model trains on n - 1 samples and is evaluated on the one remaining held-out row. This process repeats n times so that every single data point serves as the test set exactly once.
While numerical evaluation scores tell us how well a model performs, learning curves reveal why it succeeds or fails. By tracking performance across changing sample sizes or complexity settings, learning curves operationalize the bias-variance framework into visual diagnostics.
Two complementary plotting habits diagnose different failure modes:
Plot train error (decreasing curve) and validation error (decreasing then plateau) against the number of training rows:
For KNN, vary k (small k = more complex) on the X axis against train + val accuracy on the Y axis.
Pick the complexity where validation set accuracy is maximal (or validation loss minimal).
n_jobs parallelizes across CPU cores: n_jobs = 1 → sequential; n_jobs = 2 → two folds at once on 2 CPUs; n_jobs = −1 → all available CPUs.
Grid search = brute-force exhaustive sweep over a user-specified Cartesian grid of hyperparameter combinations. For each combination, run K-Fold CV and record its mean CV score; then pick the combination with the best score.
Classify each item as a Parameter or a Hyperparameter.
(A) In a Decision Tree: the maximum depth restriction.
(B) In Linear/Logistic Regression: the learned coefficient β₁ (the slope).
(C) In Neural Networks: the learning rate (α) of gradient descent.
Five mini-scenarios. For each, answer: is this allowed ML practice, or does it leak data / invalidate the test score?
Six labeled 1-D points: X = [1, 2, 3, 4, 5, 6] and y = [A, B, A, B, A, B].
Fold 1 = {1,2}, Fold 2 = {3,4}, Fold 3 = {5,6}. Use 1-NN. Compute per-fold accuracy, then mean CV accuracy.
(i) Fold 1 Test: Train on {3,4,5,6}, Test on {1:A, 2:B}.
Nearest to X=1 among {3,4,5,6} is X=3 (A) → predict A (Correct).
Nearest to X=2 among {3,4,5,6} is X=3 (A) → predict A (Wrong).
Accuracy = 1/2 = 0.5
(ii) Fold 2 Test: Train on {1,2,5,6}, Test on {3:A, 4:B}.
Nearest to X=3 among {1,2,5,6} is X=2 (B) → predict B (Wrong).
Nearest to X=4 among {1,2,5,6} is X=5 (A) → predict A (Wrong).
Accuracy = 0/2 = 0.0
(iii) Fold 3 Test: Train on {1,2,3,4}, Test on {5:A, 6:B}.
Nearest to X=5 among {1,2,3,4} is X=4 (B) → predict B (Wrong).
Nearest to X=6 among {1,2,3,4} is X=4 (B) → predict B (Correct).
Accuracy = 1/2 = 0.5
Mean CV Accuracy = (0.5 + 0.0 + 0.5) / 3 ≈ 0.333.
Insight: A crucial detail here is that the nearest neighbor must come from
the training fold only — never from the other test point in the
same fold.
Three learning-curve scenarios. Match each to its diagnosis and recommendation:
Small n = 150 labeled training rows. Perform a stratified k = 5 stratified CV.
(a) 150 / 5 = 30 rows per fold.
(b) 150 − 30 = 120 training rows per iteration.
(c) 5 folds → 5 separate model fits → 5 models. (Then +1 final refit on all 150 rows once hyperparameters are chosen, for a total of 6 fits.)
(d)
Report: CV accuracy = 88.0% (± 3.7%).
(a) 10 → 10 fits (plus 1 final refit = 11).
(b) LOOCV = n = 81 rows → 81 fits (plus 1 refit → 82 total).
(c) (i) n = 1000: 10-fold clearly better — 10 models instead of 1000, plus each fold has ~900 training rows which is plenty; LOOCV would be overkill. (ii) n = 15: 10-fold leaves only 1–2 test samples per fold — scores unreliable. LOOCV trains on 14 rows, tests 1, no randomness → better estimate for tiny datasets; prefer LOOCV!
On a small 2-D toy binary problem, you test KNN with k = 1, 3, 7, 15 and measure both training accuracy and 5-fold CV (validation) accuracy: {k, train, CV} triples are {1, 1.00, 0.62}, {3, 0.95, 0.78}, {7, 0.88, 0.85}, {15, 0.78, 0.77}.
(a) k = 1: train accuracy 100% (memorized) — CV only 62% with big gap → classic overfitting / high variance. k = 15: both errors are fairly high but close together → underfitting / high bias (too smooth, ignoring local structure). k = 3 & k = 7: moving toward just right as k rises to 7.
(b) Pick k = 7. It has the maximum cross-validation accuracy = 85%, with train (88%) and CV (85%) only 3 pp apart → low gap, low overfit.
(c) CV accuracy: k = 1 → 0.62, k = 3 → 0.78, k = 7 → 0.85 (peak!), k = 15 → 0.77 (falling back). That inverted-U shape is the complexity curve in action.
GridSearchCV with param_grid = { n_neighbors: [5, 11, 19, 27, 35], weights: ['uniform', 'distance'], metric: ['euclidean', 'manhattan', 'chebyshev'] }. Stratified 5-fold CV.
(a) Cartesian product: 5 k values × 2 weights × 3 metrics = 30 combinations.
(b) Each combination has 5 CV folds → 5 fits. 30 × 5 = 150 fits (plus 1 final refit on winner → 151 total).
(c) Sequential: 150 × 0.2 s = 30 s. With 8 CPUs in parallel: ~30/8 ≈ 3.75 seconds (plus small overhead — very fast!). This is one of grid search's advantages — it's embarrassingly parallel.
Dataset of 2,500 rows. Use 64/16/20 train/val/test split.
(a) 2500 × 0.64 = 1,600 train; × 0.16 = 400 val; × 0.20 = 500 test.
(b) The validation set — or via k-fold on the combined train+val (2,000 rows).
(c) Train + Validation combined (2,000 rows).
(d) Evaluate exactly once on the 500-row test set. One single number — that is the reported generalization accuracy.
You compare four KNN classifiers on a binary classification task: k ∈ {3, 7, 15, 31}. 5-fold CV gives fold accuracies below:
| k | Fold1 | Fold2 | Fold3 | Fold4 | Fold5 |
|---|---|---|---|---|---|
| 3 | 0.85 | 0.82 | 0.88 | 0.80 | 0.85 |
| 7 | 0.89 | 0.86 | 0.90 | 0.87 | 0.88 |
| 15 | 0.88 | 0.89 | 0.86 | 0.91 | 0.91 |
| 31 | 0.82 | 0.83 | 0.81 | 0.84 | 0.85 |
(a) Means:
(b) SD(k=7): values around 0.880 → devs [+0.01, −0.02, +0.02, −0.01, 0.00] → var = 0.00025 → SD = 0.0158. SD(k=15): mean = 0.890 → devs [−0.01, 0, −0.03, +0.02, +0.02] → var = 0.00045 → SD ≈ 0.0212. k = 7 slightly more stable; both good. Winner k = 15 wins by mean accuracy.
(c) Retrain a single KNN(k=15) classifier on the full TRAIN+VAL combined dataset, then evaluate exactly once on the held-out test set. Report that single value as your generalization accuracy.
4 points: X = [1, 2, 4, 5]; y = [A, A, B, B]. 1-NN classifier. Compute LOOCV accuracy.
Iter 1: test 1 (A). Train on {2(A), 4(B), 5(B)}. Nearest of 1 is 2(A) → predict A correct ✔
Iter 2: test 2 (A). Train {1(A), 4(B), 5(B)}. Nearest is 1(A) → predict A correct ✔
Iter 3: test 4 (B). Train {1(A), 2(A), 5(B)}. Nearest is 5(B) → predict B correct ✔
Iter 4: test 5 (B). Train {1(A), 2(A), 4(B)}. Nearest is 4(B) → predict B correct ✔
LOOCV accuracy = 4 / 4 = 1.00 (100%).
A neural network gives training loss 0.001, validation loss 0.65. Your colleague suggests: "We just need more labeled data." Critique that suggestion by (a) naming the actual syndrome, then (b) giving three concrete interventions that address it directly, and (c) identifying one diagnostic observation on the curve that would actually justify "get more data."
(a) Classic high-variance / overfitting (huge train/val gap). (b) Three fixes from the menu: (i) simplify architecture (fewer layers/neurons), (ii) add dropout or weight regularization, (iii) add data augmentation / noise, (iv) apply early stopping, (v) feature selection to remove noisy inputs, (vi) decrease model complexity (e.g., bigger k if it were KNN). (c) "Need more data" is justified only when the validation loss curve is still decreasing at the right edge of the training-set-size X-axis and not yet plateaued. If it's flat with a big gap, more rows won't close it — the model is too flexible.
You want to compare KNN hyperparameters but also need to standardize features. Why is Pipeline([('sc', StandardScaler()), ('clf', KNeighborsClassifier())]) required inside GridSearchCV instead of scaling once at the top level? Give the one-sentence leakage explanation, then write the param_grid format with pipeline namespaced keys.
Leakage explanation: Scaling before CV means each fold's StandardScaler was fit using test-fold rows as part of its mean/SD — the validation fold's distribution statistics leak into training, producing optimistically biased CV scores. The pipeline re-fits scaler + classifier on each fold's training split only, so CV is honest.
Namespaced grid format:
Answer all 7 questions. Click an option for instant feedback.
Your score: 0 / 7